import numpy as np
from math import cos, sin, tan, fabs
from enum import Enum
from triangle import Triangle

__all__ = ['Rasterizer']


class Buffers(Enum):
    Color = 1
    Depth = 2

    def __or__(self, other):
        return self.value | other.value

    def __and__(self, other):
        return self.value & other.value


# 光栅化器
class Rasterizer:
    next_id = 0
    pos_buf = {}
    ind_buf = {}
    times = 100

    def __init__(self, width, height):
        self.frame_buf = np.zeros((height * width, 3), np.float32)
        self.depth_buf = np.zeros(height * width)
        self.monitor_width = width
        self.monitor_height = height

    def set_mvp(self, model, view, projection):
        # noinspection PyAttributeOutsideInit
        self.mvp = np.dot(projection, np.dot(view, model))

    def get_next_id(self):
        t = self.next_id
        self.next_id += 1
        return t

    def set_pixel(self, point, color):
        if (point[0] < 0 or point[0] >= self.monitor_width or
                point[1] < 0 or point[1] >= self.monitor_height):
            return
        index = (self.monitor_height - point[1] - 1) * self.monitor_width + point[0]
        self.frame_buf[int(index)] = color

    def load_positions(self, pos: list) -> int:
        pos_id = self.get_next_id()
        self.pos_buf[pos_id] = pos
        return pos_id

    def load_indices(self, index: list):
        ind_id = self.get_next_id()
        self.ind_buf[ind_id] = np.array([index])
        return ind_id

    def clear(self):
        depth_buf_len = len(self.depth_buf)
        frame_buf_len = len(self.frame_buf)
        for i in range(frame_buf_len):
            self.frame_buf[i] = np.array([0, 0, 0])

        for i in range(depth_buf_len):
            self.depth_buf[i] = float("inf")

    def draw(self, pos_id, ind_id):

        buf = self.pos_buf[pos_id]
        pos_index = self.ind_buf[ind_id]

        f1 = (100 - 0.1) / 2
        f2 = (100 + 0.1) / 2

        # 右结合性
        mvp = self.mvp

        for index in pos_index:
            t = Triangle()
            vertex_vector = [np.dot(mvp, np.append(buf[index[0]], 1)),
                             np.dot(mvp, np.append(buf[index[1]], 1)),
                             np.dot(mvp, np.append(buf[index[2]], 1))]

            vertex_vector = np.array(vertex_vector)

            for vec in vertex_vector:
                vec /= vec[3]

            for i in vertex_vector:
                i[0] = 0.5 * self.monitor_width * (i[0] + 1.0)
                i[1] = 0.5 * self.monitor_height * (i[1] + 1.0)
                i[2] = i[2] * f1 + f2

            for i in range(3):
                t.set_vertex(i, vertex_vector[i][:3])

            t.set_color(0, 255, 0, 0)
            t.set_color(1, 0, 255, 0)
            t.set_color(2, 0, 0, 255)

            self.rasterize_wireframe(t)

    def rasterize_wireframe(self, t):
        self.draw_line(t.c(), t.a())
        self.draw_line(t.c(), t.b())
        self.draw_line(t.b(), t.a())

    # Bresenham's line drawing algorithm
    # Code taken from a stack overflow answer: https://stackoverflow.com/a/16405254
    def draw_line(self, begin, end):
        x1, y1 = map(int, [begin[0], begin[1]])
        x2, y2 = map(int, (end[0], end[1]))

        line_color = np.array([255, 255, 255])

        dx = int(x2 - x1)
        dy = int(y2 - y1)
        dx1 = int(fabs(dx))
        dy1 = int(fabs(dy))
        px = int(2 * dy1 - dx1)
        py = int(2 * dx1 - dy1)

        if dy1 <= dx1:
            if dx >= 0:
                x, y, xe = x1, y1, x2
            else:
                x, y, xe = x2, y2, x1
            point = np.array([x, y, 1.0])
            self.set_pixel(point, line_color)
            while x < xe:
                x += 1
                if px < 0:
                    px += 2 * dy1
                else:
                    if (dx < 0 and dy < 0) or (dx > 0 and dy > 0):
                        y += 1
                    else:
                        y -= 1
                    px += 2 * (dy1 - dx1)
                point = np.array([x, y, 1.0])
                self.set_pixel(point, line_color)
        else:
            if dy >= 0:
                x, y, ye = x1, y1, y2
            else:
                x, y, ye = x2, y2, y1
            point = np.array([x, y, 1.0])
            self.set_pixel(point, line_color)
            while y < ye:
                y += 1
                if py <= 0:
                    py += 2 * dx1
                else:
                    if (dx < 0 and dy < 0) or (dx > 0 and dy > 0):
                        x += 1
                    else:
                        x -= 1
                    py += 2 * (dx1 - dy1)
                point = np.array([x, y, 1.0])
                self.set_pixel(point, line_color)

    def convert_to_8uc3(self):
        image = self.frame_buf.reshape(700, 700, 3).copy().astype(np.uint8)
        return image
